/*
 * Copyright 2010 Gaurav Saxena
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.gwtstructs.gwt.client.widgets.multicolumnlistbox;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.HasChangeHandlers;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.VerticalPanel;

public class MultiColumnListBox<T> extends FocusPanel implements HasChangeHandlers{
	private HorizontalPanel enclosingPanel = new HorizontalPanel();
	private ArrayList<VerticalPanel> optionsPanel = new ArrayList<VerticalPanel>();
	private List<ListBoxItem<T>> listBoxItems;
	private int numberOfItemsPerColumn;
	private boolean isMultiSelect = true;
	private HTML startDragOption;
	private boolean startDragging;
	private int shiftStartIndex = -1;
	/**Initializes {@link MultiColumnListBox}
	 * @param numberOfItemsPerColumn
	 */
	public MultiColumnListBox(int numberOfItemsPerColumn) {
		this(new ArrayList<ListBoxItem<T>>(), numberOfItemsPerColumn);
	}
	/**
	 * Initializes {@link MultiColumnListBox}
	 * @param listBoxItems
	 * @param numberOfItemsPerColumn should be greater than 0
	 */
	public MultiColumnListBox(List<ListBoxItem<T>> listBoxItems, int numberOfItemsPerColumn) {
		ScrollPanel enclosingScrollPanel = new ScrollPanel();
		super.add(enclosingScrollPanel);
		enclosingScrollPanel.add(enclosingPanel);
		enclosingScrollPanel.setWidth("100%");
		enclosingScrollPanel.setHeight("100%");
		this.listBoxItems = listBoxItems;
		this.numberOfItemsPerColumn = numberOfItemsPerColumn;
		if(numberOfItemsPerColumn < 1)
			throw new IllegalArgumentException("Number of Items per columns should be more than 0");
		setUpListItems();
		setStyleName("gwtstrutcs-multicolumnselectbox");
	}
	/**
	 * Initializes {@link MultiColumnListBox} with number of items per column = 10
	 * @param listBoxItems
	 */
	public MultiColumnListBox(List<ListBoxItem<T>> listBoxItems) {
		this(listBoxItems, 10);
	}
	@Override
	public void onLoad(){
		super.onLoad();
		Timer t = new Timer(){
			@Override
			public void run() {
				if(enclosingPanel.getOffsetHeight() < getOffsetHeight())
					setHeight(enclosingPanel.getOffsetHeight() + "px");
			}
		};
		t.schedule(50);
	}
	private void setUpListItems() {
		boolean seenFirstSelected = false;
		if(!listBoxItems.isEmpty()) {
			for(int i = 0; i < listBoxItems.size(); i++) {
				HTML option = new HTML(listBoxItems.get(i).getDisplayNameHtml());
				DOM.setElementAttribute(option.getElement(), "value", listBoxItems.get(i).getValue().toString());
				DOM.setStyleAttribute(option.getElement(), "cursor", "default");//not working for chrome
				if(listBoxItems.get(i).isSelected()) {
					if(isMultiSelect || (!isMultiSelect && !seenFirstSelected)) {
						option.getElement().addClassName("gwtstrutcs-multicolumnselectbox-selected");
						seenFirstSelected = true;
					}
				} else
					option.getElement().addClassName("gwtstrutcs-multicolumnselectbox-unselected");
				addMouseDownHandlerToOption(option, listBoxItems.get(i), i);
				setUpOption(option);
				int j = i / numberOfItemsPerColumn;
				if(optionsPanel.size() <= j) {
					VerticalPanel verticalPanel = new VerticalPanel();
					verticalPanel.setWidth("100%");
					verticalPanel.setStyleName("gwtstrutcs-multicolumnselectbox-columns");
					enclosingPanel.add(verticalPanel);
					optionsPanel.add(verticalPanel);
				}
				optionsPanel.get(j).add(option);
			}
			startDragOption = (HTML) optionsPanel.get(0).getWidget(0);
		}
	}
	private void addMouseDownHandlerToOption(final HTML option, final ListBoxItem<T> currentItem, final int currentIndex) {
		option.addMouseDownHandler(new MouseDownHandler(){
			@Override
			public void onMouseDown(MouseDownEvent event) {
				if(isMultiSelect && event.getNativeEvent().getShiftKey()) {
					int max = Math.max(shiftStartIndex, currentIndex);
					int min = Math.min(shiftStartIndex, currentIndex);
					boolean isSelected = listBoxItems.get(shiftStartIndex).isSelected();
					for(int i = 0; i < listBoxItems.size(); i++) {
						if(i < min || i > max) {
							listBoxItems.get(i).setSelected(false);
							makeSelected(false, (HTML) optionsPanel.get(i / numberOfItemsPerColumn).getWidget(i % numberOfItemsPerColumn));
						} else {
							listBoxItems.get(i).setSelected(true);
							makeSelected(true, (HTML) optionsPanel.get(i / numberOfItemsPerColumn).getWidget(i % numberOfItemsPerColumn));
						}
					}
					listBoxItems.get(shiftStartIndex).setSelected(isSelected);
					makeSelected(isSelected, (HTML) optionsPanel.get(shiftStartIndex / numberOfItemsPerColumn).getWidget(shiftStartIndex % numberOfItemsPerColumn));
					
				} else if(isMultiSelect && event.getNativeEvent().getCtrlKey()) {
					shiftStartIndex = currentIndex;
					if(!currentItem.isSelected()) {
						makeSelected(true, option);
						currentItem.setSelected(true);
					} else {
						makeSelected(false, option);
						currentItem.setSelected(false);
					}
				} else {
					deselectAll();
					shiftStartIndex = currentIndex;
					makeSelected(true, option);
					currentItem.setSelected(true);
				}
				if(isMultiSelect) {
					startDragOption = option;
					startDragging = true;
				}
			}
		});		
	}
	private void setUpOption(final HTML option) {
		option.addMouseUpHandler(new MouseUpHandler(){
			@Override
			public void onMouseUp(MouseUpEvent event) {
				startDragging = false;
				fireEvent(new ChangeEvent(){});
		}});
		if(isMultiSelect) {
			option.addMouseOverHandler(new MouseOverHandler(){
				@Override
				public void onMouseOver(MouseOverEvent event) {
					if(startDragging && isMultiSelect)
					{
						int startRow = 0, startColumn = 0;
						for(int i = 0; i < optionsPanel.size(); i++)
						{
							startColumn = i;
							startRow = optionsPanel.get(i).getWidgetIndex(startDragOption);
							if(startRow > -1)
								break;
						}
						int endRow = 0, endColumn = 0;
						for(int i = 0; i < optionsPanel.size(); i++)
						{
							endColumn = i;
							endRow = optionsPanel.get(i).getWidgetIndex(option);
							if(endRow > -1)
								break;
						}
						if(startRow > endRow)
						{
							int temp;
							temp = startRow;
							startRow = endRow;
							endRow = temp;
						}
						if(startColumn > endColumn)
						{
							int temp;
							temp = startColumn;
							startColumn = endColumn;
							endColumn = temp;
						}
						boolean isSelected;
						for(int i = 0; i < numberOfItemsPerColumn; i++)
						{
							for(int j = 0; j < optionsPanel.size(); j++)
							{
								if(i >= startRow && i <= endRow && j >= startColumn && j <= endColumn)
									isSelected = true;
								else
									isSelected = false;
								if(j * numberOfItemsPerColumn + i < listBoxItems.size())
								{
									makeSelected(isSelected, (HTML) optionsPanel.get(j).getWidget(i));
									listBoxItems.get(j * numberOfItemsPerColumn + i).setSelected(isSelected);
								}
							}
						}
					}
				}
			});
		}
		option.addMouseMoveHandler(new MouseMoveHandler(){
			@Override
			public void onMouseMove(MouseMoveEvent event) {
				emptySelection();
			}});		
	}
	private void deselectAll() {
		for(int i = 0; i < optionsPanel.size(); i++)
			for(int j = 0; j < optionsPanel.get(i).getWidgetCount(); j++)
				makeSelected(false, (HTML) optionsPanel.get(i).getWidget(j));
		for(int i = 0; i < listBoxItems.size(); i++)
			listBoxItems.get(i).setSelected(false);
		
	}
	private void makeSelected(boolean isSelected, HTML option) {
		if(isSelected)
		{
			option.getElement().removeClassName("gwtstrutcs-multicolumnselectbox-unselected");
			option.getElement().addClassName("gwtstrutcs-multicolumnselectbox-selected");
		}
		else
		{
			option.getElement().removeClassName("gwtstrutcs-multicolumnselectbox-selected");
			option.getElement().addClassName("gwtstrutcs-multicolumnselectbox-unselected");
		}
	}
	/**
	 * returns true if more than one options can be selected in {@link MultiColumnListBox}, otherwise false
	 * @return
	 */
	public boolean isMultiSelect() {
		return isMultiSelect;
	}
	/**
	 * Sets the {@link MultiColumnListBox} to be able to select multiple items
	 * @param isMultiSelect
	 */
	public void setMultiSelect(boolean isMultiSelect) {
		this.isMultiSelect = isMultiSelect;
		if(!isMultiSelect) {
			resetListBox();
			setUpListItems();
		}
	}
	public HandlerRegistration addChangeHandler(ChangeHandler handler) {
	    return addDomHandler(handler, ChangeEvent.getType());
	}
	/**
	  * Adds an item to the list box.
	  * @param item the {@link ListBoxItem} to be added
	  */
	public void addItem(ListBoxItem<T> item) {
		addItem(item, listBoxItems.size());
	}
	
	/**
	 * Adds an item to the list box.
	 * @param item item the {@link ListBoxItem} to be added
	 * @param position the position of the item
	 */
	public void addItem(ListBoxItem<T> item, int position) {
		resetListBox();
	    listBoxItems.add(position, item);
	    setUpListItems();
	}
	private void resetListBox() {
		 enclosingPanel.clear();
		 optionsPanel = new ArrayList<VerticalPanel>();
	}
	/**
	  * Removes all items from the list box.
	  */
	 public void clear() {
		 enclosingPanel.clear();
		 optionsPanel = new ArrayList<VerticalPanel>();
		 listBoxItems = new ArrayList<ListBoxItem<T>>();
		 setUpListItems();
	 }
	 /**
	  * Gets the number of items present in the list box.
	  * 
	  * @return the number of items
	  */
	 public int getItemCount() {
		 return listBoxItems.size();
	 }
	 /**
	   * Gets the text associated with the item at the specified index.
	   * 
	   * @param index the index of the item whose text is to be retrieved
	   * @return the text associated with the item
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public String getItemText(int index) {
	    checkIndex(index);
	    return DOM.getInnerText(getOption(index).getElement());
	  }
	  /**
	   * Gets the text associated with the item at the specified index.
	   * 
	   * @param index the index of the item whose text is to be retrieved
	   * @return the text associated with the item
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public String getItemHtml(int index) {
	    checkIndex(index);
	    return listBoxItems.get(index).getDisplayNameHtml();
	  }
	  /**
	   * Gets the value associated with the item at a given index.
	   * 
	   * @param index the index of the item to be retrieved
	   * @return the item's associated value
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public T getValue(int index) {
		  checkIndex(index);
		    return listBoxItems.get(index).getValue();
	  }
	  private void checkIndex(int index) {
	    if (index < 0 || index >= listBoxItems.size()) {
	      throw new IndexOutOfBoundsException();
	    }
	  }
	  /**
	   * Determines whether an individual list item is selected.
	   * 
	   * @param index the index of the item to be tested
	   * @return <code>true</code> if the item is selected
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public boolean isItemSelected(int index) {
	    checkIndex(index);
	    return listBoxItems.get(index).isSelected();
	  }
	  /**
	   * Removes the item at the specified index.
	   * 
	   * @param index the index of the item to be removed
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public void removeItem(int index) {
		  checkIndex(index);
		  listBoxItems.remove(index);
		  resetListBox();
		  setUpListItems();
	  }
	  /**
	   * Sets whether an individual list item is selected.
	   * 
	   * <p>
	   * Note that setting the selection programmatically does <em>not</em> cause
	   * the {@link ChangeHandler#onChange(ChangeEvent)} event to be fired.
	   * </p>
	   * 
	   * @param index the index of the item to be selected or unselected
	   * @param selected <code>true</code> to select the item
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public void setItemSelected(int index, boolean selected) {
	    checkIndex(index);
	    if(!isMultiSelect && selected)
	    	deselectAll();
	    listBoxItems.get(index).setSelected(selected);
	    makeSelected(selected, getOption(index));
	  }
	  private HTML getOption(int index) {
		  return (HTML) optionsPanel.get(index / numberOfItemsPerColumn).getWidget(index % numberOfItemsPerColumn);
	  }
	/**
	  * Sets the text associated with the item at a given index.
	  * 
	  * @param index the index of the item to be set
	  * @param text the item's new text
	  * @throws IndexOutOfBoundsException if the index is out of range
	  */
	  public void setItemHtml(int index, String html) {
	    checkIndex(index);
	    if (html == null) {
	      throw new NullPointerException("Cannot set an option to have null text");
	    }
	    listBoxItems.get(index).setDisplayNameHtml(html);
	    DOM.setInnerHTML(getOption(index).getElement(), html);
	  }
	  /**
	   * Sets the value associated with the item at a given index. 
	   * 
	   * @param index the index of the item to be set
	   * @param value the item's new value; cannot be <code>null</code>
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public void setValue(int index, T value) {
		  //This value can be
		  // * used for any purpose, but is also what is passed to the server when the
		  // * list box is submitted as part of a {@link FormPanel}.
		  checkIndex(index);
		  DOM.setElementAttribute(getOption(index).getElement(), "value", value.toString());
		  listBoxItems.get(index).setValue(value);
	  }
	  private native void emptySelection()/*-{
		  	if($wnd.document.selection && document.selection.empty)
				$wnd.document.selection.empty();
			else if($wnd.getSelection)
				$wnd.getSelection().removeAllRanges();
	  }-*/;
	public List<ListBoxItem<T>> getListBoxItems() {
		return listBoxItems;
	}
	public void setListBoxItems(List<ListBoxItem<T>> listBoxItems) {
		this.listBoxItems = listBoxItems;
		resetListBox();
		setUpListItems();
	}
	public int getNumberOfItemsPerColumn() {
		return numberOfItemsPerColumn;
	}
	public void setNumberOfItemsPerColumn(int numberOfItemsPerColumn) {
		this.numberOfItemsPerColumn = numberOfItemsPerColumn;
		resetListBox();
		setUpListItems();
	}
	/**
	 * Sets width of the table containing options. This may overflow the listbox if its width is less
	 * @param width in pixels
	 */
	public void setOptionTableWidth(int width)
	{
		enclosingPanel.setWidth(width + "px");
	}
}
